#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import json
import copy
import bnetd
import pathlib
import asyncio
from datetime import datetime


class I2pObConnType(bnetd.ObConnPluginBase):

    def __init__(self, plugin_id, plugin_description):
        super().__init__(plugin_id, plugin_description)
        self._context = None
        self._uuid = None

    @property
    def need_temp_directory(self):
        return True

    @property
    def need_data_directory(self):
        return True

    def set_context(self, context):
        assert self._context is None and context is not None
        self._context = context

    async def on_init(self):
        cfgFullfn = os.path.join(self._context.CFG_OBCONN_DIR, "connection.i2p.cfg")

        cfg = None
        try:
            # supports only one connection in config directory
            cfg = self._cfgParseFullfnAndCheck(cfgFullfn)
        except FileNotFoundError:
            return

        if self._uuid is not None and self._uuid != cfg["uuid"]:
            raise bnetd.InvalidCfgError(cfgFullfn, "multiple connection UUID found")

        obj = I2pObConn(self, cfg, self._context["temp-directory"], self._context["data-directory"])
        if obj.is_in_gateway() != self._context.is_in_gateway():
            # connection config file only in /etc, so we don't use /etc/bnetd/settings.json
            return

        await self._context.on_obconn_available(obj)
        self._uuid = cfg["uuid"]

    @staticmethod
    def _cfgParseFullfnAndCheck(cfgPath):
        # read cfg
        cfg = None
        try:
            with open(cfgPath) as f:
                cfg = json.load(f)
        except json.JSONDecodeError:
            raise bnetd.InvalidCfgError(cfgPath, "illegal format")

        # check cfg
        if True:
            cfg2 = copy.deepcopy(cfg)

            if "uuid" in cfg2:
                del cfg2["uuid"]
            else:
                raise bnetd.InvalidCfgError(cfgPath, "missing uuid property")

            if "name" in cfg2:
                del cfg2["name"]
            else:
                raise bnetd.InvalidCfgError(cfgPath, "missing name property")

            if "description" in cfg2:
                del cfg2["description"]

            if "in-gateway" in cfg2:
                if cfg2["in-gateway"] not in ["true", "false"]:
                    raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for in-gateway property' % (cfg2["in-gateway"]))
                del cfg2["in-gateway"]

            if "auto-connect" in cfg2:
                if cfg2["auto-connect"] not in ["true", "false"]:
                    raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for auto-connect property' % (cfg2["auto-connect"]))
                del cfg2["auto-connect"]

            if "outproxy" in cfg2:
                if cfg2["outproxy"] not in _HTTP_OUT_DICT or cfg2["outproxy"] not in _SOCKS_OUT_DICT:
                    raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for outproxy property' % (cfg2["outproxy"]))
                del cfg2["outproxy"]

            if len(cfg2) > 0:
                raise bnetd.InvalidCfgError(cfgPath, "unknown property: %s" % (list(cfg2.keys())[0]))

        return cfg


class I2pObConn(bnetd.ObConnBase):

    def __init__(self, plugin, cfg, tmpDir, dataDir):
        super().__init__(plugin, bnetd.PluginObjectSource.CFG)
        self._cfg = cfg
        self._tmpDir = tmpDir
        self._dataDir = dataDir

    def get_uuid(self):
        return self._cfg["uuid"]

    def get_name(self):
        return self._cfg["name"]
    
    def get_description(self):
        return self._cfg.get("description", "")

    def get_egress_depends(self):
        return bnetd.EgressDependency("")

    def get_egress_provides(self):
        ret = [
            bnetd.EgressDomainHost(".i2p"),
            bnetd.EgressDomainService(".i2p", "web"),
        ]
        if "outproxy" in self._cfg:
            ret += [
                bnetd.EgressIpService("0.0.0.0/0", "web"),
                bnetd.EgressDomainService(".", "web", exclude_list=[".union"]),
            ]
        return ret

    def is_in_gateway(self):
        return self._cfg.get("in-gateway", "false") == "true"

    def is_auto_connect(self):
        return self._cfg.get("auto-connect", "false") == "true"

    async def activate(self, stage_id, stage_data):
        assert stage_id == "init"

        dtNow = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")

        buf = ""
        buf += "pidfile = %s\n" % (os.path.join(self._tmpDir, "i2pd.pid"))
        buf += "logfile = %s\n" % (os.path.join(self._dataDir, "i2pd.log.%s" % (dtNow)))
        buf += "tunconf = %s\n" % (os.path.join(self._dataDir, "tunnels.conf"))
        buf += "tunnelsdir = %s\n" % (os.path.join(self._dataDir, "tunnels.d"))
        buf += "\n"
        buf += "ipv4 = true\n"
        buf += "ipv6 = true\n"
        buf += "ssu = true\n"
        buf += "\n"
        if True:
            buf += "[ntcp2]\n"
            buf += "\n"
        if True:
            buf += "[ssu2]\n"
            buf += "\n"
        if True:
            buf += "[http]\n"
            buf += "address = 127.0.0.1\n"
            buf += "port = 7070\n"
            buf += "\n"
        if True:
            buf += "[httpproxy]\n"
            buf += "address = 127.0.0.1\n"
            buf += "port = 4444\n"
            if self._cfg.get("outproxy", None) in _HTTP_OUT_DICT:
                buf += "outproxy = %s\n" % (_HTTP_OUT_DICT[self._cfg["outproxy"]])
            buf += "outbound.length = 1\n"
            buf += "inbound.length = 1\n"
            buf += "\n"
        if True:
            buf += "[socksproxy]\n"
            buf += "address = 127.0.0.1\n"
            buf += "port = 4447\n"
            if self._cfg.get("outproxy", None) in _SOCKS_OUT_DICT:
                buf += "outproxy.enabled = true\n"
                buf += "outproxy = %s\n" % (_SOCKS_OUT_DICT[self._cfg["outproxy"]][0])
                buf += "outproxyport = %d\n" % (_SOCKS_OUT_DICT[self._cfg["outproxy"]][1])
            buf += "outbound.length = 1\n"
            buf += "inbound.length = 1\n"
            buf += "\n"
        if True:
            buf += "[sam]\n"
            buf += "enabled = true\n"
            buf += "\n"
        if True:
            buf += "[bob]\n"
            buf += "\n"
        if True:
            buf += "[i2cp]\n"
            buf += "\n"
        if True:
            buf += "[i2pcontrol]\n"
            buf += "\n"
        if True:
            buf += "[precomputation]\n"
            buf += "\n"
        if True:                                    # FIXME
            buf += "[upnp]\n"
            buf += "enabled = true\n"
            buf += "name = I2Pd\n"
            buf += "\n"
        if True:
            buf += "[meshnets]\n"
            buf += "\n"
        if True:
            buf += "[reseed]\n"
            buf += "verify = true\n"
            buf += "\n"
        if True:
            buf += "[addressbook]\n"
            buf += "\n"
        if True:
            buf += "[limits]\n"
            buf += "\n"
        if True:
            buf += "[trust]\n"
            buf += "\n"
        if True:
            buf += "[exploratory]\n"
            buf += "\n"
        if True:
            buf += "[persist]\n"
            buf += "\n"
        if True:
            buf += "[cpuext]\n"
            buf += "\n"

        cfgFullfn = os.path.join(self._tmpDir, "i2pd.conf")
        pathlib.Path(cfgFullfn).write_text(buf)

        outFullfn = os.path.join(self._dataDir, "i2pd.out.%s" % (dtNow))
        self._proc = await asyncio.create_subprocess_shell("i2pd --service --conf %s --datadir %s >%s 2>&1" % (cfgFullfn, self._dataDir, outFullfn))

        # FIXME:
        # should ensure that i2p website can be accessed by proxy, using await
        # should ensure that popular website can be accessed by proxy if outproxy is set, using await
        # timeout is 10 minutes

    async def deactivate(self):
        assert False

    async def dispose(self):
        self._proc.terminate()
        await self._proc.wait()
        del self._proc


_HTTP_OUT_DICT = {
    "acetone": "http://outproxy.acetone.i2p:3128"
}


_SOCKS_OUT_DICT = {
    "acetone": ("outproxy.acetone.i2p", 1080)
}


#outproxy = http://false.i2p
#outproxy = http://exit.stormycloud.i2p
#outproxy = http://bc437iyumrwoo242dtdjenosvnqgs4svzxo2eq5iehk6vzezw3qa.b32.i2p:8889
#outproxy = http://4uhjrmilirueyiaps7qshnyhbamyxusyjcbiy6lwh7oq5wyqisbq.b32.i2p:8888
